package org.biogroovy.io.eutils;

import groovy.util.logging.Slf4j

import java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat

import javax.xml.namespace.QName
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.*

import org.biogroovy.conf.BioGroovyConfig
import org.biogroovy.eutils.EUtilsURLFactory
import org.biogroovy.io.AbsXmlReader
import org.biogroovy.io.IFetcher;
import org.biogroovy.models.*
import org.w3c.dom.Node
import org.w3c.dom.NodeList

/**
 * This class reads a PubMed article from NCBI's eUtils REST service.
 */
@Slf4j
class PubMedArticleReader extends AbsXmlReader<Article> {
	
	static final String DATABASE_NAME = "pubmed";

	/** A map containing the XPath expressions for various fields. */
	static final Map<String, String> XPATH_MAP = [
		pubmedId:'MedlineCitation/PMID',
		dateCreated:'PubmedData/History/PubMedPubDate',
		title:'MedlineCitation/Article/ArticleTitle',

		///PubmedArticleSet/PubmedArticle/
		abs:'MedlineCitation/Article/Abstract/AbstractText',
		author:'MedlineCitation/Article/AuthorList/Author',
		journal:'MedlineCitation/Article/Journal',
		doi: 'PubmedData/ArticleIdList/ArticleId[@IdType="doi"]',
		meshHeadings:'MedlineCitation/MeshHeadingList/MeshHeading'

	];

	/** A map containing the node types for each of the various fields.*/
	static final Map<String, QName> NODE_TYPE_MAP = [
		pubmedId:XPathConstants.STRING,
		abs:XPathConstants.STRING,
		title:XPathConstants.STRING,
		doi:XPathConstants.STRING

	]

	static final String ROOT_PATH = "//PubmedArticleSet/PubmedArticle";
	
	/** The name of the tool as defined in the biogroovy.conf file. */
	String tool = null;
	
	/** The email address of the user as defined in the biogroovy.conf file. */
	String email = null;
	
	/**
	 * Constructor.
	 */
	public PubMedArticleReader(){
		this.databaseName = DATABASE_NAME;
		ConfigObject obj = BioGroovyConfig.getConfig();
		tool = obj.eutils.tool
		email = obj.eutils.email
	}
	
	
	@Override
	public List<Article> fetchAll(String id) throws IOException {
		
		URL url = getUrl(id, [tool:this.tool, email:this.email]);
		log.debug("fetching article: " + url);
		List<Article> articleList = readList(url.openStream());
		return articleList;
	}

	@Override
	public URL getUrl(String id, Map<String, String> paramMap) {
		Map<String, String> map = [db:EUtilsURLFactory.DB_PUBMED, id:id, retmode:'xml']
		map.putAll(paramMap);
		String url = EUtilsURLFactory.getURL( EUtilsURLFactory.EFETCH, map);
		return new URL(url);
	}



	@Override
	public Article readFile(File file) {
		log.debug("reading file: " + file);
		if (!file.exists()){
			throw new FileNotFoundException("The file was not found: ${file.getName()}")
		}
		return read(new FileInputStream(file));
	}

	@Override
	public List<Article> readList(InputStream inputStream){
		List<Article> articleList = new ArrayList<Article>();

		def builder  = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		def root     = builder.parse(inputStream).documentElement


		XPath xpath = XPathFactory.newInstance().newXPath();
		NodeList nodeList = (NodeList)xpath.evaluate(ROOT_PATH, root, XPathConstants.NODESET);
		for(int i=0; i < nodeList.getLength(); i++){
			Node node = nodeList.item(i)
			Article article = new Article();
			parse(article, node);
			articleList.add(article);
		}

		builder = null;
		root = null;

		return articleList;
	}

	@Override
	public void parse(Article article, Node node) {
		parseData(node, article, XPATH_MAP, NODE_TYPE_MAP);
		parseDate(node, "dateCreated",article);
		parseAuthorList(node, article);
		parseMeshHeadings(node, article);
		parseJournal(node, article);
	}

	@Override
	public Article read(InputStream is) {
		List<Article> articleList = readList(is);
		return articleList?.get(0);
	}

	/**
	 * This method fetches an article from the EUtils database.
	 * @param id  a single PubMed ID.
	 * @return a PubMed article
	 */
	public Article fetch(String id) {
		URL url = getUrl(id, [tool:this.tool, email:this.email])
		List<Article> articleList = readList(url.openStream());
		return articleList?.get(0);
	}


	private void parseJournal(Node root, Article article){
		XPath xpath = XPathFactory.newInstance().newXPath();
		def journalNode = xpath.evaluate(XPATH_MAP.journal, root, XPathConstants.NODE);
		assert journalNode != null;

		XPath xpathJournalName = XPathFactory.newInstance().newXPath();
		XPath xpathJournalVol = XPathFactory.newInstance().newXPath();
		XPath xpathJournalIss = XPathFactory.newInstance().newXPath();

		Journal journal = new Journal();
		journal.title = xpathJournalName.evaluate('Title',journalNode, XPathConstants.STRING)
		journal.volume = xpathJournalVol.evaluate('JournalIssue/Volume',journalNode, XPathConstants.STRING)
		journal.issue = xpathJournalIss.evaluate('JournalIssue/Issue',journalNode, XPathConstants.STRING)
		journal.publicationDate = new Date();
		journal.publicationDate.year = xpathJournalName.evaluate('./JournalIssue/Issue',journalNode, XPathConstants.NUMBER)
		journal.publicationDate.month = xpathJournalName.evaluate('./JournalIssue/Issue',journalNode, XPathConstants.NUMBER) - 1;
		journal.issn = xpathJournalName.evaluate("./ISSN", journalNode, XPathConstants.STRING)
		article.journal = journal;
	}

	private void parseAuthorList(Node root, Article article){
		XPath xpath = XPathFactory.newInstance().newXPath();
		NodeList nodeSet =  xpath.evaluate( XPATH_MAP.author, root, XPathConstants.NODESET );
		XPath xpathFN = XPathFactory.newInstance().newXPath();
		XPath xpathLN = XPathFactory.newInstance().newXPath();
		XPath xpathInit = XPathFactory.newInstance().newXPath();
		nodeSet.each{
			Author auth = new Author();
			auth.firstname = xpathFN.evaluate("./ForeName", it, XPathConstants.STRING);
			auth.lastname = xpathFN.evaluate("./LastName", it, XPathConstants.STRING);
			auth.initials = xpathInit.evaluate("./Initials", it, XPathConstants.STRING);

			article.authors.add(auth);
		}
	}

	private void parseMeshHeadings(Node root, Article article){
		XPath xpath = XPathFactory.newInstance().newXPath();
		XPath xpathDesc = XPathFactory.newInstance().newXPath();
		XPath xpathQual = XPathFactory.newInstance().newXPath();
		NodeList nodeSet =  xpath.evaluate( XPATH_MAP.meshHeadings, root, XPathConstants.NODESET );

		nodeSet.each{

			MeshHeading mesh = new MeshHeading();
			mesh.descriptorName = xpathDesc.evaluate("./DescriptorName", it, XPathConstants.STRING)
			NodeList qualNodeSet = xpathQual.evaluate("./QualifierName", it, XPathConstants.NODESET);

			if (qualNodeSet != null){
				for (int i = 0; i < qualNodeSet.getLength(); i++) {
					mesh.qualifierNames.add(qualNodeSet.item(i).textContent)
				}
			}

			article.meshHeadings.add(mesh)
		}

	}

	private void parseDate(Node root, String propertyName, Article article){
		String value = null;
		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();
		String year = xpath.evaluate( XPATH_MAP.get(propertyName) + "/Year", root , XPathConstants.STRING);
		String month = xpath.evaluate( XPATH_MAP.get(propertyName) + "/Month", root , XPathConstants.STRING);
		String day = xpath.evaluate( XPATH_MAP.get(propertyName) + "/Day", root , XPathConstants.STRING);
		DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");

		try {
			Date date = dateFormat.parse("${year}/${month}/${day}")
			article.setProperty(propertyName, date)
		} catch(ParseException pe){
			log.debug(pe.getMessage(), pe);
		}
	}


	@Override
	public IFetcher<Article> getNewInstance() {
		return new PubMedArticleReader();
	}






}
